import os
import sys
import errno
from PyQt4 import QtCore
from limber.documents import document


class FileWrapperDocument(QtCore.QObject, document.AbstractDocument):

    SPECIAL_NAMES = (
        'stdin',
        'stdout',
        'stderr',
        '/dev/null',
        'devnull',
        '/dev/zero',
        'devzero',
        '/dev/full',
        'devfull',
        '/dev/random',
        'devrandom',
        )

    SPECIAL_STREAMS = {
        'stdin' : sys.__stdin__,
        'stdout' : sys.__stdout__,
        'stderr' : sys.__stderr__,
        }

    SPECIAL_DEVICES_NAMES = {
        '/dev/null' : os.devnull,
        'devnull' : os.devnull,
        '/dev/zero' : '/dev/zero',
        'devzero' : '/dev/zero',
        '/dev/full' : '/dev/full',
        'devfull' : '/dev/full',
        '/dev/random' : '/dev/random',
        'devrandom' : '/dev/random',
        }

    document_modified = QtCore.pyqtSignal(name='documentModified')
    document_saved = QtCore.pyqtSignal(name='documentSaved')
    document_closed = QtCore.pyqtSignal(name='documentClosed')
    document_loaded = QtCore.pyqtSignal(name='documentLoaded')

    def __init__(self, parent=None, path=None, mode=None):
        super(FileWrapperDocument, self).__init__(parent)

        doc_index = FileWrapperDocument.__mro__.index(document.AbstractDocument)
        super(FileWrapperDocument.__mro__[doc_index - 1], self).__init__(path, mode)

    def _is_valid_mode(self, mode):
        '''Return `True` if *mode* can be used to open a file.'''

        modes = ('r', 'w', 'a', 'U',
                 'r+', 'w+', 'a+', 'U+',
                 'w+t')
        bin_modes = ['%sb' % m for m in modes]

        return mode in modes or mode in bin_modes

    def can_read(self, mode=None):
        '''Return `True` if the given mode allows reading.'''

        if mode is None:
            mode = self._mode
        return mode not in ('w', 'a') and mode is not None

    def can_write(self, mode=None):
        '''Return `True` if the given mode allows writing.'''

        if mode is None:
            mode = self._mode
        return mode not in ('r', 'U', 'rU', 'rb') and mode is not None

    def open(self, path, mode='r'):
        '''Open a given path.'''

        if not self._is_valid_mode(mode):
            raise ValueError('Invalid document mode: %r' % mode)

        if not path.startswith('file:///'):
            self._data = open(path, mode)
            self.set_path(path)
            self._mode = mode
            self._is_closed = False
            self._modification_flag = False
            self._encoding = self._data.encoding
            self.document_loaded.emit()
        else:
            special_name = path[len('file:///'):]
            if special_name not in self.SPECIAL_NAMES:
                raise ValueError('Invalid special name: %r.' % special_name)

            self.open_device(special_name, mode)

    def open_device(self, device, mode='r'):
        '''Opens the given device using the right mode.'''

        if device in ('stdin', 'stdout', 'stderr'):
            valid_modes = (
                ('stdin', ('r', 'rb')),
                ('stdout', ('w', 'wb', 'a')),
                ('stderr', ('w', 'wb', 'a')),
                )
            for devname, valids in valid_modes:
                if device == devname:
                    if mode not in valids:
                        raise ValueError('Invalid mode %r for device %r.'
                                         % (mode, device))
                    else:
                        break

            self._data = self.SPECIAL_STREAMS[device]
            self.set_path('file:///%s' % device)
            self._mode = mode
            self._is_closed = self._data.closed
            self._modification_flag = False
            self._encoding = self._data.encoding
        else:
            try:
                self._data = open(self.SPECIAL_DEVICES_NAMES[device], mode)
                self.set_path('file:///%s' % self._data.name)
                self._mode = mode
                self._is_closed = self._data.closed
                self._modification_flag = False
                self._encoding = self._data.encoding
            except KeyError:
                raise ValueError('Invalid device name: %r.' % device)

        self.document_loaded.emit()

    def close(self):
        '''Close the document.'''

        try:
            self._data.close()
            self._data = None
            self._is_closed = True
            self._modification_flag = False
            self.document_closed.emit()
        except AttributeError:
            pass

    def flush(self):
        '''Flushes the document.'''

        try:
            self._data.flush()
            self._modification_flag = False
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def save(self):
        '''Save the current document.'''

        try:
            cur_pos = self._data.tell()
            self._data.seek(0)
            outfil = open(self._path, 'w+t')
            for line in self._data:
                outfil.write(line)
            outfil.close()
            self._data.seek(cur_pos)
            self.document_saved.emit()
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def reload(self):
        '''Reloads the document.'''

        self.open(self._path, self._mode)

    def read(self, bytes=-1):
        '''Read *bytes* bytes from the document.'''

        try:
            return self._data.read(bytes)
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def write(self, data):
        '''Writes *data* to the document.'''

        try:
            self._data.write(data)
            self.document_modified.emit()
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def tell(self):
        '''Return the current position in the file.'''

        try:
            return self._data.tell()
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def seek(self, pos, mode=0):
        '''Jumps to the given position.'''

        try:
            self._data.seek(pos, mode)
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def readline(self, linesep='\n'):
        '''Return a file.'''

        if self._data is None:
            raise ValueError('I/O operation on closed file.')
        if not linesep:
            return self.read()
        elif len(linesep) == 1:
            chars = []
            while True:
                char = self._data.read(1)
                chars.append(char)
                if char == linesep:
                    break
            return ''.join(chars)
        else:
            toks = tuple(linesep)
            index = 0
            chars = []
            while True:
                char = self._data.read(1)
                chars.append(char)
                if char == toks[index]:
                    index += 1
                else:
                    index = 0

                if index == len(linesep):
                    break
            return ''.join(chars)

    def writelines(self, lines, end=''):
        '''Write each line of *lines* to the file.'''

        try:
            if not end:
                for line in lines:
                    self._data.write(line)
            else:
                for line in ('%s%s' % (L, end) for L in lines):
                    self._data.write(line)
            self.document_modified.emit()
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def truncate(self, size=None):
        '''Truncate the file after *size* bytes.'''

        if size is None:
            size = self.tell()

        try:
            self._data.truncate(size)
            self.document_modified.emit()
        except AttributeError:
            raise ValueError('I/O operation on closed file.')

    def isatty(self):
        '''Return `True` if the document is interactive.'''

        try:
            return self._data.isatty()
        except AttributeError:
            if self._data is None:
                raise ValueError('I/O operation on closed file.')
            return False
